/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/graphics/dx11.1/details/common.hpp>
#include <baja/graphics/dx11.1/graphics_fwd.hpp>
#include <baja/graphics/dx11.1/details/enum_mappings.hpp>

#include <baja/graphics/dx11.1/details/texture2d_impl.hpp>
#include <baja/graphics/dx11.1/details/dxtex_helper.hpp>
#include <baja/graphics/dx11.1/details/util.hpp>

#include <baja/stream/stream.hpp>

namespace baja { namespace graphics { namespace dx11_1 {

namespace d = baja::graphics::dx11_1::details;

class texture : public itexture, public b::iunknown_provider
{
    std::shared_ptr<d::texture2d> _texture;

public:
    texture(const std::shared_ptr<d::texture2d>& texture) : _texture(texture) {}

    static std::shared_ptr<texture> create(const std::shared_ptr<d::texture2d>& t)    
    {
        std::shared_ptr<texture> returnValue = std::make_shared<texture>(t);
        return returnValue;
    }
    
    // b::iunknown_provider
    
    virtual size_t getInterfaceCount()
    {
        return _texture->getInterfaceCount();
    }

    virtual WRL::ComPtr<IUnknown> getInterface(size_t index)
    {
        return _texture->getInterface(index);
    }
    
    // itexture
    
    virtual uint32 getWidth()
    {
        return _texture->getWidth();
    }
    
    virtual uint32 getHeight()
    {
        return _texture->getHeight();
    }
    
    virtual pixel_format getPixelFormat()
    {
        return _texture->getPixelFormat();
    }

    virtual uint32 getBitsPerPixel()
    {
        return _texture->getBitsPerPixel();
    }

    virtual uint32 getArraySize()
    {
        return _texture->getArraySize();
    }

    virtual uint32 getMipLevelCount()
    {
        return _texture->getMipLevelCount();
    }

    virtual size_t calculateSize(
        const uint32 sourceWidth,
        const uint32 sourceHeight
        )
    {
        return _texture->calculateSize(sourceWidth, sourceHeight);
    }

    virtual void  copyToSystemMemory(
        const uint32 mipLevel,
        const uint32 sourceX,
        const uint32 sourceY,
        const uint32 sourceWidth,
        const uint32 sourceHeight,
        memory* p
        )
    {
        return _texture->copyToSystemMemory(mipLevel, 
            sourceX, sourceY, sourceWidth, sourceHeight, p
            );
    }

    virtual void copyFromSystemMemory(
        const uint32 mipLevel,
        const uint32 destX,
        const uint32 destY,
        const uint32 dataX,
        const uint32 dataY,
        const uint32 dataWidth,
        const uint32 dataHeight,
        const memory* data,
        const uint32 dataSizeInBytes,
        const uint32 dataBytesPerPixel,
        const uint32 pitch
        )
    {
        return _texture->copyFromSystemMemory(
            mipLevel, 
            destX, destY, dataX, dataY, dataWidth, dataHeight,
            data, dataSizeInBytes, dataBytesPerPixel, pitch
            );
    }

    virtual void lock(const lock_type lockType, memory** data, uint32* pitch)
    {
        return _texture->lock(0, lockType, data, pitch);
    }
    
    virtual void unlock()
    {
        return _texture->unlock();
    }
};

// creates a texture wrapper over the specified texture
inline std::shared_ptr<itexture> createTextureFromTexture2d(
    ID3D11Texture2D* texture2d
    )
{
    BAJA_CHECK_ARG(texture2d != nullptr);

    // wrap the view into d::texture2d object
    std::shared_ptr<d::texture2d> textureWrapper = d::texture2d::create(texture2d);

    std::shared_ptr<texture> returnValue = texture::create(textureWrapper);
    return std::dynamic_pointer_cast<itexture, texture>(returnValue);
}

// creates a texture from the specified views
inline std::shared_ptr<itexture> createTextureFromViews(
    const std::vector<WRL::ComPtr<ID3D11View>>& views
    )
{
    BAJA_CHECK_ARG(views.size() > 0);

    // wrap the view into d::texture2d object
    std::shared_ptr<d::texture2d> textureWrapper = d::texture2d::create(views);

    std::shared_ptr<texture> returnValue = texture::create(textureWrapper);
    return std::dynamic_pointer_cast<itexture, texture>(returnValue);
}

inline void 
mapPoolToD3DSettings(
    const uint32 usage,
    memory_pool pool,
    D3D11_USAGE* d3d11Usage,
    uint32* cpuAccess,
    uint32* d3d11Binding
    )
{
    BAJA_CHECK_ARG(d3d11Usage != NULL);
    BAJA_CHECK_ARG(cpuAccess != NULL);
    BAJA_CHECK_ARG(d3d11Binding != NULL);
    
    *d3d11Usage = D3D11_USAGE_DEFAULT;

    *cpuAccess = 0;
    if (pool == memory_pool::system)
    {
        *d3d11Usage = D3D11_USAGE_STAGING;
        *cpuAccess = D3D11_CPU_ACCESS_READ|D3D11_CPU_ACCESS_WRITE;
    }
    else if ((usage & static_cast<uint32>(texture_usage::dynamic)) == static_cast<uint32>(texture_usage::dynamic))
    {
        *d3d11Usage = D3D11_USAGE_DYNAMIC;
        *cpuAccess = D3D11_CPU_ACCESS_WRITE;
    }

    // determine binding
    *d3d11Binding = 0;
    if (pool != memory_pool::system)
    {
        *d3d11Binding = D3D11_BIND_SHADER_RESOURCE;

        // may also need to bind as render target and/or depth stencil
        if ((usage & static_cast<uint32>(texture_usage::renderTarget)) == static_cast<uint32>(texture_usage::renderTarget))
        {
            *d3d11Binding |= D3D11_BIND_RENDER_TARGET;
        }

        if ((usage & static_cast<uint32>(texture_usage::depthStencil)) == static_cast<uint32>(texture_usage::depthStencil))
        {
            *d3d11Binding |= D3D11_BIND_DEPTH_STENCIL;
        }
    }
    else
    {
        *d3d11Binding = 0;
    }
}

// Creates a texture with the given properties
inline std::shared_ptr<itexture> 
engine::createTexture( 
    const uint32 width,
    const uint32 height,
    const uint32 mipLevels,
    const uint32 usage,
    const pixel_format format,
    const memory_pool pool,
    const uint32 msaaCount,
    const uint32 msaaQuality
    )
{ 
    D3D11_USAGE d3d11Usage = D3D11_USAGE_DEFAULT;
    uint32 cpuAccess = 0;
    uint32 d3d11Binding = 0;
    mapPoolToD3DSettings(usage, pool,
        &d3d11Usage, &cpuAccess, &d3d11Binding
        );

    uint32 miscFlags = 0;
    if ((usage & static_cast<uint32>(texture_usage::genMipMaps)) == static_cast<uint32>(texture_usage::genMipMaps))
    {
        miscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS;
        d3d11Binding |= (D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE);
    }

    // create new texture
    WRL::ComPtr<ID3D11Texture2D> newTexture;
    newTexture = d::createTexture2DObject(
        _d3d11Device.Get(),
        width,
        height,
        mipLevels,
        format,
        msaaCount,
        msaaQuality,
        d3d11Usage,
        d3d11Binding,
        cpuAccess,
        miscFlags
        );

    win32_result wr;

    // create view(s) on texture
    std::vector<WRL::ComPtr<ID3D11View>> views;

    // if d3d11Binding is 0, it means there are no bindings and hence we should not resize the vector.
    if (d3d11Binding != 0)
    {
        views.resize(4);
        if ((d3d11Binding & D3D11_BIND_UNORDERED_ACCESS) == D3D11_BIND_UNORDERED_ACCESS)
        {
            WRL::ComPtr<ID3D11UnorderedAccessView> view;
            wr = _d3d11Device->CreateUnorderedAccessView(newTexture.Get(), NULL, view.GetAddressOf());
            views[static_cast<int>(texture_type::undorderedAccess)] = view;
        }

        if ((d3d11Binding & D3D11_BIND_SHADER_RESOURCE) == D3D11_BIND_SHADER_RESOURCE)
        {
            WRL::ComPtr<ID3D11ShaderResourceView> view;
            wr = _d3d11Device->CreateShaderResourceView(newTexture.Get(), NULL, view.GetAddressOf());
            views[static_cast<int>(texture_type::shaderResource)] = view;
        }

        if ((d3d11Binding & D3D11_BIND_RENDER_TARGET) == D3D11_BIND_RENDER_TARGET)
        {
            WRL::ComPtr<ID3D11RenderTargetView> view;
            wr = _d3d11Device->CreateRenderTargetView(newTexture.Get(), NULL, view.GetAddressOf());
            views[static_cast<int>(texture_type::renderTarget)] = view;
        }

        if ((d3d11Binding & D3D11_BIND_DEPTH_STENCIL) == D3D11_BIND_DEPTH_STENCIL)
        {
            WRL::ComPtr<ID3D11DepthStencilView> view;
            wr = _d3d11Device->CreateDepthStencilView(newTexture.Get(), NULL, view.GetAddressOf());
            views[static_cast<int>(texture_type::depthStencil)] = view;
        }
    }

    std::shared_ptr<itexture> texture;

    if (views.size() > 0)
    {
        // wrap the view into a texture
        texture = createTextureFromViews(views);
    }
    else
    {
        texture = createTextureFromTexture2d(newTexture.Get());
    }

    return texture;
}

// Creates a texture from the specified filename
inline concurrency::task<std::shared_ptr<g::itexture>> 
engine::createTextureFromFileAsync(
    const wchar* filename,
    const uint32 usage,
    const pixel_format format,
    const memory_pool pool,
    const mipmap_load mipmapLoading
    )
{
    BAJA_CHECK_ARG(filename);
    
    std::shared_ptr<engine> p_this = std::dynamic_pointer_cast<engine>(shared_from_this());
    return createMemoryStreamFromFileAsync(filename).then(
        [p_this, usage, format, pool, mipmapLoading] (const std::shared_ptr<imemory_stream>& ms) -> std::shared_ptr<g::itexture>
        {
            return p_this->createTextureFromFileInMemory(
                (memory*)ms->getBuffer(), 
                static_cast<uint32>(ms->getBufferSize()),
                usage,
                format,
                pool,
                mipmapLoading
                );

        });
}

inline void
engine::createTextureFromFileAsync(
    const std::shared_ptr<baja::storage::istorage_file>& file,
    const uint32 usage,
    const pixel_format format,
    const memory_pool pool,
    const mipmap_load mipmapLoading,
    std::function<void(bool success, const std::shared_ptr<g::itexture>& texture)> createCompletedFunc
    )
{
    std::shared_ptr<engine> p_this = std::dynamic_pointer_cast<engine>(shared_from_this());

    file->createMemoryStreamFromFileAsync(
        [p_this, usage, format, pool, mipmapLoading, createCompletedFunc] (bool success, const std::shared_ptr<imemory_stream>& ms)
        {
            if (!success)
            {
                BAJA_THROW(std::logic_error("an error occured while executing createMemoryStreamFromFileAsync()"));
            }
            createCompletedFunc(
                success,
                p_this->createTextureFromFileInMemory(
                    (memory*)ms->getBuffer(), 
                    static_cast<uint32>(ms->getBufferSize()),
                    usage,
                    format,
                    pool,
                    mipmapLoading
                    ));
        });
}

// Creates a texture from the specified filename
inline std::shared_ptr<itexture> engine::createTextureFromFile(
    const wchar* filename,
    const uint32 usage,
    const pixel_format format,
    const memory_pool pool,
    const mipmap_load mipmapLoading
    )
{
    BAJA_CHECK_ARG(filename);
    
    std::shared_ptr<imemory_stream> fileStream;
    fileStream = createMemoryStreamFromFile(filename);

    return this->createTextureFromFileInMemory(
        (memory*)fileStream->getBuffer(), 
        static_cast<uint32>(fileStream->getBufferSize()),
        usage,
        format,
        pool,
        mipmapLoading
        );
}

// Creates a texture from a file already loaded into memory
inline std::shared_ptr<itexture> engine::createTextureFromFileInMemory( 
    const memory* data,
    const uint32 dataSize,
    const uint32 usage,
    const pixel_format format,
    const memory_pool pool,
    const mipmap_load mipmapLoading
    )
{
    BAJA_CHECK_ARG(data);

    win32_result wr;

    DirectX::ScratchImage image;
    DirectX::loadFromFileInMemory(data, dataSize, &image);
    
    // this instance needs to represent a 2D texture
    wr = (image.GetMetadata().dimension == DirectX::TEX_DIMENSION_TEXTURE2D); // ERROR:
    
    D3D11_USAGE d3d11Usage = D3D11_USAGE_DEFAULT;
    uint32 cpuAccess = 0;
    uint32 d3d11Binding = 0;
    mapPoolToD3DSettings(
        usage, 
        pool,
        &d3d11Usage, 
        &cpuAccess, 
        &d3d11Binding
        );

    uint32 miscFlags = 0;
    if ((usage & static_cast<uint32>(texture_usage::genMipMaps)) == static_cast<uint32>(texture_usage::genMipMaps))
    {
        miscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS;
    }

    // load the texture resource
    WRL::ComPtr<ID3D11Resource> loadedResource;
    loadedResource = baja::graphics::dx11_1::details::createTextureFromSystemMemory(
        _d3d11Device.Get(),
        d::toDXGI_FORMAT(format),
        mipmapLoading,
        d3d11Usage,
        d3d11Binding,
        cpuAccess,
        miscFlags,
        image
        );

    // QI into texture interface
    WRL::ComPtr<ID3D11Texture2D> newTexture;
    loadedResource.As(&newTexture);

    std::shared_ptr<itexture> texture;

    std::vector<WRL::ComPtr<ID3D11View>> views;
    views.resize(4);  
            
    if ((d3d11Binding & D3D11_BIND_SHADER_RESOURCE) == D3D11_BIND_SHADER_RESOURCE)
    {
        WRL::ComPtr<ID3D11ShaderResourceView> view;
        wr = _d3d11Device->CreateShaderResourceView(newTexture.Get(), NULL, view.GetAddressOf());
        views[static_cast<int>(texture_type::shaderResource)] = view;
    }

    if ((d3d11Binding & D3D11_BIND_RENDER_TARGET) == D3D11_BIND_RENDER_TARGET)
    {
        WRL::ComPtr<ID3D11RenderTargetView> view;
        wr = _d3d11Device->CreateRenderTargetView(newTexture.Get(), NULL, view.GetAddressOf());
        views[static_cast<int>(texture_type::renderTarget)] = view;
    }

    if ((d3d11Binding & D3D11_BIND_DEPTH_STENCIL) == D3D11_BIND_DEPTH_STENCIL)
    {
        WRL::ComPtr<ID3D11DepthStencilView> view;
        wr = _d3d11Device->CreateDepthStencilView(newTexture.Get(), NULL, view.GetAddressOf());
        views[static_cast<int>(texture_type::depthStencil)] = view;
    }

    if (views.size() > 0)
    {
        // wrap the view into a texture
        texture = createTextureFromViews(views);
    }
    else
    {
        texture = createTextureFromTexture2d(newTexture.Get());
    }

    return texture; 
}

}}} // namespace baja.graphics.dx11_1
